﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;
using System.Linq;

namespace FileRename
{
    class MP3Manipulator
    {
        internal static void Rename(string folder, string searchPattern, string replacementPattern)
        {
            if (string.IsNullOrEmpty(folder))
            {
                throw new ArgumentNullException("folder");
            }
            if (string.IsNullOrEmpty(searchPattern))
            {
                throw new ArgumentNullException("searchPattern");
            }
            if (string.IsNullOrEmpty(replacementPattern))
            {
                throw new ArgumentNullException("replacementPattern");
            }

            Regex matchRegex = new Regex(searchPattern);

            foreach (string fileName in GetFilesFromDirectoryByRegexFilter(folder, searchPattern))
            {
                string newFileName = matchRegex.Replace(fileName, replacementPattern);

                if (fileName != newFileName)
                {
                    string srcRename = Path.Combine(folder, fileName);
                    string destRename = Path.Combine(folder, newFileName);

                    try
                    {
                        File.Move(srcRename, destRename);
                    }
                    catch (Exception e)
                    {
                        System.Windows.Forms.MessageBox.Show(
                            string.Format("Error while renaming {0}{0}{1}{0}to{0}{2}{0}{0}Error:{0}{3}", Environment.NewLine, srcRename, destRename, e),
                            "Error renaming file",
                            System.Windows.Forms.MessageBoxButtons.OK,
                            System.Windows.Forms.MessageBoxIcon.Error);
                    }
                }
            }
        }

        internal static IEnumerable<IAnalyzer> AnalyzeForRussification(string folder, string searchPattern, bool includeFolders, bool transliterate, Encoding sourceEncoding, Encoding destinationEncoding)
        {
            foreach (string file in GetFilesFromDirectoryByRegexFilter(folder, searchPattern, includeFolders))
            {
                IAnalyzer analyzer = null;
                if (transliterate)
                {
                    if (Directory.Exists(file))
                    {
                        analyzer = new DirectoryTransliterateAnalysis(file);
                    }
                    else
                    {
                        analyzer = new MP3TransliterateAnalysis(file);
                    }
                }
                else
                {
                    if (Directory.Exists(file))
                    {
                        analyzer = new DirectoryRussifyAnalysis(file);
                    }
                    else
                    {
                        analyzer = new MP3RussifyAnalysis(file);
                    }
                }

                analyzer.SourceEncoding = sourceEncoding;
                analyzer.DestinationEncoding = destinationEncoding;

                analyzer.Analyze();

                yield return analyzer;
            }
        }

        internal static IEnumerable<RenameFileByTag> AnalyzeForRenameByTag(string folder, string searchPattern, string renamePattern)
        {
            foreach (string file in GetFilesFromDirectoryByRegexFilter(folder, searchPattern))
            {
                RenameFileByTag russifyAnalysis = new RenameFileByTag(file, searchPattern, renamePattern);
                yield return russifyAnalysis;
            }
        }

        private static IEnumerable<string> GetFilesFromDirectoryByRegexFilter(string folder, string searchPattern, bool includeFolders = false)
        {
            string argumentExceptionMessage = string.Empty;

            Regex matchRegex = null;
            try
            {
                matchRegex = new Regex(searchPattern);
            }
            catch (ArgumentException e)
            {
                argumentExceptionMessage = string.IsNullOrEmpty(e.Message) ? "ERROR: Unknown" : e.Message;
            }
            catch (Exception)
            {
                throw;
            }

            if (!string.IsNullOrEmpty(argumentExceptionMessage))
            {
                yield return "ERROR: " + argumentExceptionMessage;
            }

            if (includeFolders)
            {
                string[] directories = Directory.GetDirectories(folder, "*", SearchOption.AllDirectories);
                foreach (string directory in directories)
                {
                    yield return directory;
                }
            }

            string[] files = Directory.GetFiles(folder, "*", includeFolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);

            foreach (string fileName in files)
            {
                if (matchRegex.IsMatch(fileName))
                {
                    yield return fileName;
                }
            }
        }
    }

    internal interface IAnalyzer
    {
        Encoding SourceEncoding { get; set; }
        Encoding DestinationEncoding { get; set; }

        void ApplyAnalysis();
        void Analyze();

        string FilenamePath { get; }
        string FilenameOnlyProposed { get; }
        string FilenameProposed { get; }
        string SongNameCurrent { get; }
        string AlbumCurrent { get; }
        string ArtistCurrent { get; }
        string SongNameProposed { get; }
        string AlbumProposed { get; }
        string ArtistProposed { get; }
    }

    internal class DirectoryRussifyAnalysis : BaseRussifier, IAnalyzer
    {
        public string FilenamePath { get; private set; }
        private string FileNameOnly
        {
            get { return Path.GetDirectoryName(this.FilenamePath); }
        }

        public string FilenameOnlyProposed
        {
            get { return Path.GetFileName(this.FilenameProposed); }
            set
            {
                string sourceFileName = string.IsNullOrEmpty(this.FilenameProposed) ? this.FilenamePath : this.FilenameProposed;
                string fullPath = Path.GetDirectoryName(sourceFileName);
                this.FilenameProposed = fullPath;
            }
        }

        public string FilenameProposed { get; private set; }

        public string SongNameCurrent { get; private set; }
        public string AlbumCurrent { get; private set; }
        public string ArtistCurrent { get; private set; }
        public string SongNameProposed { get; private set; }
        public string AlbumProposed { get; private set; }
        public string ArtistProposed { get; private set; }

        internal DirectoryRussifyAnalysis(string directory)
        {
        }

        public void Analyze()
        {
            if (extCharsReges.IsMatch(this.FileNameOnly))
            {
                this.FilenameOnlyProposed = ConvertToActualRussian(this.FileNameOnly);
            }
        }


        public void ApplyAnalysis()
        {
            if (!string.IsNullOrEmpty(this.FilenamePath) && !string.IsNullOrEmpty(this.FilenameProposed))
            {
                Directory.Move(this.FilenamePath, this.FilenameProposed);
                this.FilenamePath = this.FilenameProposed;
            }
        }

    }

    internal class MP3RussifyAnalysis : BaseRussifier, IAnalyzer
    {
        public string FilenamePath { get; private set; }

        private string FileNameOnly
        {
            get { return Path.GetFileName(this.FilenamePath); }
        }

        public string FilenameOnlyProposed
        {
            get { return Path.GetFileName(this.FilenameProposed); }
            set
            {
                string sourceFileName = string.IsNullOrEmpty(this.FilenameProposed) ? this.FilenamePath : this.FilenameProposed;
                string fullPath = Path.GetDirectoryName(sourceFileName);
                this.FilenameProposed = Path.Combine(fullPath, value);
            }
        }

        public string FilenameProposed { get; private set; }

        public string SongNameCurrent { get; private set; }
        public string AlbumCurrent { get; private set; }
        public string ArtistCurrent { get; private set; }

        public string SongNameProposed { get; private set; }
        public string AlbumProposed { get; private set; }
        public string ArtistProposed { get; private set; }

        internal MP3RussifyAnalysis(string filename)
        {
            this.FilenamePath = filename;
        }

        public void Analyze()
        {
            using (TagLib.File f = TagLib.File.Create(FilenamePath))
            {
                this.SongNameCurrent = f.Tag.Title;
                this.AlbumCurrent = f.Tag.Album;
                StringBuilder sb = new StringBuilder();
                foreach (string performer in f.Tag.Performers)
                {
                    if (sb.Length > 0)
                    {
                        sb.Append(", ");
                    }

                    sb.Append(performer);
                }
                this.ArtistCurrent = sb.ToString();
            }

            CalculateProposals();
        }

        public void ApplyAnalysis()
        {
            using (TagLib.File f = TagLib.File.Create(this.FilenamePath))
            {
                if (!string.IsNullOrEmpty(this.SongNameProposed))
                {
                    f.Tag.Title = this.SongNameProposed;
                }
                if (!string.IsNullOrEmpty(this.AlbumProposed))
                {
                    f.Tag.Album = this.AlbumProposed;
                }
                if (!string.IsNullOrEmpty(this.ArtistProposed))
                {
                    f.Tag.Performers = new string[] { this.ArtistProposed };
                }

                f.Save();
            }

            if (!string.IsNullOrEmpty(this.FilenameProposed) && this.FilenamePath != this.FilenameProposed)
            {
                File.Move(this.FilenamePath, this.FilenameProposed);
                this.FilenamePath = this.FilenameProposed;
            }
        }

        private void CalculateProposals()
        {
            if (this.SongNameCurrent == null)
            {
                this.SongNameProposed = null;
            }
            else if (extCharsReges.IsMatch(this.SongNameCurrent))
            {
                this.SongNameProposed = ConvertToActualRussian(this.SongNameCurrent);
            }

            if (this.AlbumCurrent == null)
            {
                this.AlbumProposed = null;
            }
            else if (extCharsReges.IsMatch(this.AlbumCurrent))
            {
                this.AlbumProposed = ConvertToActualRussian(this.AlbumCurrent);
            }

            if (this.ArtistCurrent == null)
            {
                this.ArtistProposed = null;
            }
            else if (extCharsReges.IsMatch(this.ArtistCurrent))
            {
                this.ArtistProposed = ConvertToActualRussian(this.ArtistCurrent);
            }

            if (extCharsReges.IsMatch(this.FileNameOnly))
            {
                this.FilenameOnlyProposed = ConvertToActualRussian(this.FileNameOnly);
            }
        }
    }

    internal abstract class BaseRussifier
    {
        protected static readonly Regex extCharsReges = new Regex("[\\p{IsLatin-1Supplement}]");

        public Encoding _sourceEncoding = Encoding.GetEncoding("windows-1252");
        public Encoding _destinationEncoding = Encoding.GetEncoding("windows-1251");

        public Encoding SourceEncoding 
        {
            get { return _sourceEncoding; }
            set { _sourceEncoding = value; }
        }

        public Encoding DestinationEncoding
        {
            get { return _destinationEncoding; }
            set { _destinationEncoding = value; }
        }

        protected string ConvertToActualRussian(string input)
        {
            byte[] bytes = SourceEncoding.GetBytes(input);
            string russian = DestinationEncoding.GetString(bytes);
            return russian;
        }
    }

    internal class DirectoryTransliterateAnalysis : BaseTransliterate, IAnalyzer
    {
        public string FilenamePath { get; private set; }
        private string FileNameOnly
        {
            get
            {
                return getLeafFolder(this.FilenamePath);
            }
        }

        public string FilenameOnlyProposed
        {
            get { return Path.GetFileName(this.FilenameProposed); }
            set
            {
                string sourceFileName = string.IsNullOrEmpty(this.FilenameProposed) ? this.FilenamePath : this.FilenameProposed;
                string fullPath = Path.GetDirectoryName(sourceFileName);
                this.FilenameProposed = Path.Combine(fullPath, value);
            }
        }

        private string getLeafFolder(string fullPath)
        {
            string[] dirParts = this.FilenamePath.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Where(n => !string.IsNullOrEmpty(n)).ToArray();
            string name = dirParts.Length > 0 ? dirParts[dirParts.Length - 1] : string.Empty;
            return name;
        }

        public string FilenameProposed { get; private set; }

        public string SongNameCurrent { get; private set; }
        public string AlbumCurrent { get; private set; }
        public string ArtistCurrent { get; private set; }
        public string SongNameProposed { get; private set; }
        public string AlbumProposed { get; private set; }
        public string ArtistProposed { get; private set; }

        internal DirectoryTransliterateAnalysis(string directory)
        {
            this.FilenamePath = directory;
        }

        public void Analyze() 
        {
            if (extCharsReges.IsMatch(this.FileNameOnly))
            {
                this.FilenameOnlyProposed = Transliterate(this.FileNameOnly);
            }
        }

        public void ApplyAnalysis()
        {
            if (!string.IsNullOrEmpty(this.FilenamePath) && !string.IsNullOrEmpty(this.FilenameProposed) && this.FilenamePath != this.FilenameProposed)
            {
                Directory.Move(this.FilenamePath, this.FilenameProposed);
                this.FilenamePath = this.FilenameProposed;
            }
        }
    }

    internal class MP3TransliterateAnalysis : BaseTransliterate, IAnalyzer
    {
        public string FilenamePath { get; private set; }

        private string FileNameOnly
        {
            get { return Path.GetFileName(this.FilenamePath); }
        }

        public string FilenameOnlyProposed
        {
            get { return Path.GetFileName(this.FilenameProposed); }
            set
            {
                string sourceFileName = string.IsNullOrEmpty(this.FilenameProposed) ? this.FilenamePath : this.FilenameProposed;
                string fullPath = Path.GetDirectoryName(sourceFileName);
                this.FilenameProposed = Path.Combine(fullPath, value);
            }
        }

        public string FilenameProposed { get; private set; }

        public string SongNameCurrent { get; private set; }
        public string AlbumCurrent { get; private set; }
        public string ArtistCurrent { get; private set; }

        public string SongNameProposed { get; private set; }
        public string AlbumProposed { get; private set; }
        public string ArtistProposed { get; private set; }

        internal MP3TransliterateAnalysis(string filename)
        {
            this.FilenamePath = filename;
        }

        public void Analyze()
        {
            using (TagLib.File f = TagLib.File.Create(FilenamePath))
            {
                this.SongNameCurrent = f.Tag.Title;
                this.AlbumCurrent = f.Tag.Album;
                StringBuilder sb = new StringBuilder();
                foreach (string performer in f.Tag.Performers)
                {
                    if (sb.Length > 0)
                    {
                        sb.Append(", ");
                    }

                    sb.Append(performer);
                }
                this.ArtistCurrent = sb.ToString();
            }

            CalculateProposals();
        }

        public void ApplyAnalysis()
        {
            using (TagLib.File f = TagLib.File.Create(this.FilenamePath))
            {
                if (!string.IsNullOrEmpty(this.SongNameProposed))
                {
                    f.Tag.Title = this.SongNameProposed;
                }
                if (!string.IsNullOrEmpty(this.AlbumProposed))
                {
                    f.Tag.Album = this.AlbumProposed;
                }
                if (!string.IsNullOrEmpty(this.ArtistProposed))
                {
                    f.Tag.Performers = new string[] { this.ArtistProposed };
                }

                f.Save();
            }

            if (!string.IsNullOrEmpty(this.FilenameProposed) && this.FilenamePath != this.FilenameProposed)
            {
                File.Move(this.FilenamePath, this.FilenameProposed);
                this.FilenamePath = this.FilenameProposed;
            }
        }

        private void CalculateProposals()
        {
            if (this.SongNameCurrent == null)
            {
                this.SongNameProposed = null;
            }
            else if (extCharsReges.IsMatch(this.SongNameCurrent))
            {
                this.SongNameProposed = Transliterate(this.SongNameCurrent);
            }

            if (this.AlbumCurrent == null)
            {
                this.AlbumProposed = null;
            }
            else if (extCharsReges.IsMatch(this.AlbumCurrent))
            {
                this.AlbumProposed = Transliterate(this.AlbumCurrent);
            }

            if (this.ArtistCurrent == null)
            {
                this.ArtistProposed = null;
            }
            else if (extCharsReges.IsMatch(this.ArtistCurrent))
            {
                this.ArtistProposed = Transliterate(this.ArtistCurrent);
            }

            if (extCharsReges.IsMatch(this.FileNameOnly))
            {
                this.FilenameOnlyProposed = Transliterate(this.FileNameOnly);
            }
        }
    }


    internal abstract class BaseTransliterate
    {
        protected static readonly Regex extCharsReges = new Regex("[^\\p{IsLatin-1Supplement}]");

        public Encoding _sourceEncoding = Encoding.GetEncoding("windows-1252");
        public Encoding _destinationEncoding = Encoding.GetEncoding("windows-1251");

        public Encoding SourceEncoding
        {
            get { return _sourceEncoding; }
            set { _sourceEncoding = value; }
        }

        public Encoding DestinationEncoding
        {
            get { return _destinationEncoding; }
            set { _destinationEncoding = value; }
        }

        protected string Transliterate(string input)
        {
            StringBuilder sb = new StringBuilder();
            foreach (char letter in input)
            {
                string tc;
                switch (char.ToUpper(letter))
                {
                    case 'А': { tc = "A"; break; }
                    case 'Б': { tc = "B"; break; }
                    case 'В': { tc = "V"; break; }
                    case 'Г': { tc = "G"; break; }
                    case 'Д': { tc = "D"; break; }
                    case 'Е': { tc = "E"; break; }
                    case 'Ё': { tc = "Yo"; break; }
                    case 'Ж': { tc = "Zh"; break; }
                    case 'З': { tc = "Z"; break; }
                    case 'И': { tc = "I"; break; }
                    case 'Й': { tc = "J"; break; }
                    case 'К': { tc = "K"; break; }
                    case 'Л': { tc = "L"; break; }
                    case 'М': { tc = "M"; break; }
                    case 'Н': { tc = "N"; break; }
                    case 'О': { tc = "O"; break; }
                    case 'П': { tc = "P"; break; }
                    case 'Р': { tc = "R"; break; }
                    case 'С': { tc = "S"; break; }
                    case 'Т': { tc = "T"; break; }
                    case 'У': { tc = "U"; break; }
                    case 'Ф': { tc = "F"; break; }
                    case 'Х': { tc = "H"; break; }
                    case 'Ц': { tc = "Ts"; break; }
                    case 'Ч': { tc = "Ch"; break; }
                    case 'Ш': { tc = "Sh"; break; }
                    case 'Щ': { tc = "Sch"; break; }
                    case 'Ъ': { tc = "'"; break; }
                    case 'Ы': { tc = "I"; break; }
                    case 'Ь': { tc = "'"; break; }
                    case 'Э': { tc = "E"; break; }
                    case 'Ю': { tc = "U"; break; }
                    case 'Я': { tc = "Ya"; break; }
                    default: { tc = letter.ToString(); break; }
                }

                if (char.IsLower(letter))
                {
                    tc = tc.ToLower();
                }

                sb.Append(tc);
            }

            return sb.ToString();
        }
    }

    internal class RenameFileByTag
    {
        private readonly string currentFilename;
        private readonly bool matched;
        private readonly string proposedFilename;

        public string CurrentFilename
        {
            get
            {
                return this.currentFilename;
            }
        }

        public bool Matched
        {
            get
            {
                return this.matched;
            }
        }

        public string ProposedFilename
        {
            get
            {
                return this.proposedFilename;
            }
        }

        public void ApplyRename()
        {
            if (this.matched)
            {
                string folder = Path.GetDirectoryName(this.currentFilename);
                File.Move(this.currentFilename, Path.Combine(folder, this.proposedFilename));
            }
        }

        private readonly TagLib.Tag fileTag = null;
        private readonly string renamePattern = null;
        private readonly Regex matchRegex = null;
        private bool regexMatchExpansionErrors = false;

        internal RenameFileByTag(string file, string matchPattern, string renamePattern)
        {
            this.currentFilename = file;
            this.renamePattern = renamePattern;

            using (TagLib.File f = TagLib.File.Create(file))
            {
                this.fileTag = f.Tag;

                this.matchRegex = new Regex(matchPattern);
                string filenameonly = Path.GetFileName(file);

                if (matchRegex.IsMatch(filenameonly))
                {
                    this.proposedFilename = matchRegex.Replace(filenameonly, new MatchEvaluator(this.EvaluateMatch));
                    this.matched = !this.regexMatchExpansionErrors;
                }
                else
                {
                    this.matched = false;
                }

                this.fileTag = null;
                this.matchRegex = null;
            }
        }

        private string EvaluateMatch(Match match)
        {
            StringBuilder result = new StringBuilder();

            bool inToken = false;
            StringBuilder currentToken = new StringBuilder();

            for (int i = 0; i < this.renamePattern.Length; i++)
            {
                char currentChar = this.renamePattern[i];

                if (inToken)
                {
                    if (char.IsLetterOrDigit(currentChar))
                    {
                        currentToken.Append(currentChar);
                        continue;
                    }
                    else
                    {
                        inToken = false;
                        result.Append(GetTokenValue(currentToken.ToString(), match));
                        i--;
                        continue;
                    }
                }
                else if (currentChar == '$')
                {
                    inToken = true;
                    currentToken.Remove(0, currentToken.Length);
                    continue;
                }
                else
                {
                    result.Append(this.renamePattern[i]);
                    continue;
                }
            }

            if (inToken)
            {
                result.Append(GetTokenValue(currentToken.ToString(), match));
            }


            return result.ToString();
        }

        private string GetTokenValue(string token, Match match)
        {
            switch (token)
            {
                case "Album":
                    {
                        return fileTag.Album;
                    }
                case "Title":
                    {
                        return fileTag.Title;
                    }
                case "Track":
                    {
                        return fileTag.Track.ToString("D2");
                    }
                case "Artist":
                    {
                        return fileTag.FirstPerformer;
                    }
            }

            // if we get here, it is not one of our standard reserved ones.
            for (int grpIndex = 0; grpIndex < match.Groups.Count; grpIndex++)
            {
                Group grp = match.Groups[grpIndex];

                if (grp.Success)
                {
                    if (matchRegex.GroupNameFromNumber(grpIndex) == token)
                    {
                        return grp.Value;
                    }
                }
            }

            this.regexMatchExpansionErrors = true;
            return "<?Failed to expand '" + token + "' token as it was not found.?>";
        }
    }
}
